package com.spring.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.annotation.RequiredLog;
import com.spring.pojo.Log;
import com.spring.pojo.User;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;

/**
 * 记录用户行为日志的切面对象
 *
 * @author 黄嘉强
 * @Aspect注解描述的类为切面类型，此类型可以定义： 1）切入点 （要切入拓展业务逻辑的一些方法集合）
 * 2）通知方法 （拓展业务逻辑的实现）
 */
@Component
@Aspect
public class LogAspect {

    private static Logger log = LoggerFactory.getLogger(LogAspect.class);

    /**
     * 通过@PointCut定义切入点，@annotation是切入点表达式的一种写法，
     * 这里表示基于@RequiredLog注解描述的方法为切入点方法。
     * 这个方法内部不用写任何代码，此方法只负责承载注解的定义。
     */
    @Pointcut("@annotation(com.spring.annotation.RequiredLog)")
    public void doLog() {
    }

    /**
     * 通过@Around注解描述功能拓展方法，表示我们在此方法内部可以为目标业务进行功能拓展，
     * 可以在目标执行之前，也可以在目标执行之后。
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("doLog()")
    public Object doLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 初始状态
        int status = 1;
        // 初始错误信息
        String error = null;
        // 耗时时长
        Long time=0l;

        // 获取业务链开始时间
        Long t1 = System.currentTimeMillis();
        log.info("@Around.Start {}", t1);

        // 因为在业务链执行期间业务可能出错，所以将业务链执行的代码放入try-catch中。
        try {
            // 调用目标业务执行链（可能会有其他切面的拓展业务+目标方法业务）
            Object result = joinPoint.proceed();
            // 获取业务链结束时间
            Long t2 = System.currentTimeMillis();
            log.info("@Around.End {}", t2);
            // 获取业务总运行时长
            time = t2 - t1;
            // 如果不返回result，则响应体为空。
            // 因为上方执行链的结果就是JsonResult返回的查询到的数据，在此方法中由result接收了。
            return result;
        } catch (Exception e) {
            // 获取业务链执行出现异常的时间
            Long t3 = System.currentTimeMillis();
            // 记录出现异常的时间
            log.info("@Around.Exception {}", t3);
            time = t3 - t1;
            // 状态设置为出错（0）
            status = 0;
            // 保存错误信息
            error = e.getMessage();
            throw e;
        } finally {
            log.info("@Around.Time {}", time);
            saveLog(joinPoint, time, status, error);
        }
    }

    private void saveLog(ProceedingJoinPoint joinPoint, long time, int status, String error)
            throws NoSuchMethodException, JsonProcessingException {
        //1.获取用户行为日志
        //1.1获取ip地址
        ServletRequestAttributes requestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        String ip = request.getRemoteAddr();
        //1.2获取登陆用户
        String username = ((User) request.getSession().getAttribute("user")).getUsername();
        //1.3获取操作名
        //1.3.1获取类的字节码对象
        Class<?> targetCls = joinPoint.getTarget().getClass();
        //1.3.2获取方法签名对象
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        //1.3.3获取目标方法对象
        Method targetMethod =
                targetCls.getDeclaredMethod(
                        //方法名
                        ms.getName(),
                        //参数列表类型
                        ms.getParameterTypes());
        //1.3.4获取方法上的注解
        RequiredLog requiredLog = targetMethod.getAnnotation(RequiredLog.class);
        //1.3.5获取操作名
        String operation = requiredLog.operation();
        //1.4获取目标方法名(类全名+方法名)
        String method = targetCls.getName() + "." + targetMethod.getName();
        String params = new ObjectMapper().writeValueAsString(joinPoint.getArgs());
        //2.封装用户行为日志
        Log userLog = new Log();
        userLog.setIp(ip);
        userLog.setUsername(username);
        userLog.setCreatedTime(new Date());
        userLog.setOperation(operation);
        userLog.setMethod(method);
        userLog.setParams(params);
        userLog.setStatus(status);
        userLog.setError(error);
        userLog.setTime(time);
        //3.记录用户行为日志(控制台，文件，数据库、消息中间件)
        log.info("user log is {}", userLog);
    }
}
